/*
 * Copyright (c) Kumo Inc. and affiliates.
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <melon/json/json_test_util.h>

#include <algorithm>
#include <cmath>

#include <melon/conv.h>
#include <melon/json/json.h>
#include <melon/lang/assume.h>

namespace melon {
    bool compareJson(StringPiece json1, StringPiece json2) {
        auto obj1 = parseJson(json1);
        auto obj2 = parseJson(json2);
        return obj1 == obj2;
    }

    bool compareJsonWithNestedJson(
        StringPiece json1, StringPiece json2, unsigned strNestingDepth) {
        auto obj1 = parseJson(json1);
        auto obj2 = parseJson(json2);
        return compareDynamicWithNestedJson(obj1, obj2, strNestingDepth);
    }

    bool compareDynamicWithNestedJson(
        Dynamic const &obj1, Dynamic const &obj2, unsigned strNestingDepth) {
        if (obj1 == obj2) {
            return true;
        }
        if (strNestingDepth == 0) {
            return false;
        }
        if (obj1.type() != obj2.type()) {
            return false;
        }

        switch (obj1.type()) {
            case Dynamic::Type::ARRAY:
                if (obj1.size() != obj2.size()) {
                    return false;
                }
                for (auto i1 = obj1.begin(), i2 = obj2.begin(); i1 != obj1.end();
                     ++i1, ++i2) {
                    if (!compareDynamicWithNestedJson(*i1, *i2, strNestingDepth)) {
                        return false;
                    }
                }
                return true;
            case Dynamic::Type::OBJECT:
                if (obj1.size() != obj2.size()) {
                    return false;
                }
                return std::all_of(
                    obj1.items().begin(), obj1.items().end(), [&](const auto &item) {
                        const auto &value1 = item.second;
                        const auto value2 = obj2.get_ptr(item.first);
                        return value2 &&
                               compareDynamicWithNestedJson(value1, *value2, strNestingDepth);
                    });
            case Dynamic::Type::STRING:
                if (obj1.getString().find('{') != std::string::npos &&
                    obj2.getString().find('{') != std::string::npos) {
                    try {
                        auto nested1 = parseJson(obj1.getString());
                        auto nested2 = parseJson(obj2.getString());
                        return compareDynamicWithNestedJson(
                            nested1, nested2, strNestingDepth - 1);
                    } catch (...) {
                        // parse error, rely on basic equality check already failed
                    }
                }
                return false;
            case Dynamic::Type::NULLT:
            case Dynamic::Type::BOOL:
            case Dynamic::Type::DOUBLE:
            case Dynamic::Type::INT64:
            default:
                return false;
        }

        assume_unreachable();
    }

    namespace {
        bool isClose(double x, double y, double tolerance) {
            return std::abs(x - y) <= tolerance;
        }
    } // namespace

    bool compareDynamicWithTolerance(
        const Dynamic &obj1, const Dynamic &obj2, double tolerance) {
        if (obj1.type() != obj2.type()) {
            if (obj1.isNumber() && obj2.isNumber()) {
                const auto &integ = obj1.isInt() ? obj1 : obj2;
                const auto &doubl = obj1.isInt() ? obj2 : obj1;
                // Use to<double> to fail on precision loss for very large
                // integers (in which case the comparison does not make sense).
                return isClose(to<double>(integ.asInt()), doubl.asDouble(), tolerance);
            }
            return false;
        }

        switch (obj1.type()) {
            case Dynamic::Type::NULLT:
                return true;
            case Dynamic::Type::ARRAY:
                if (obj1.size() != obj2.size()) {
                    return false;
                }
                for (auto i1 = obj1.begin(), i2 = obj2.begin(); i1 != obj1.end();
                     ++i1, ++i2) {
                    if (!compareDynamicWithTolerance(*i1, *i2, tolerance)) {
                        return false;
                    }
                }
                return true;
            case Dynamic::Type::BOOL:
                return obj1.asBool() == obj2.asBool();
            case Dynamic::Type::DOUBLE:
                return isClose(obj1.asDouble(), obj2.asDouble(), tolerance);
            case Dynamic::Type::INT64:
                return obj1.asInt() == obj2.asInt();
            case Dynamic::Type::OBJECT:
                if (obj1.size() != obj2.size()) {
                    return false;
                }

                return std::all_of(
                    obj1.items().begin(), obj1.items().end(), [&](const auto &item) {
                        const auto &value1 = item.second;
                        const auto value2 = obj2.get_ptr(item.first);
                        return value2 &&
                               compareDynamicWithTolerance(value1, *value2, tolerance);
                    });
            case Dynamic::Type::STRING:
                return obj1.getString() == obj2.getString();
        }

        assume_unreachable();
    }

    bool compareJsonWithTolerance(
        StringPiece json1, StringPiece json2, double tolerance) {
        auto obj1 = parseJson(json1);
        auto obj2 = parseJson(json2);
        return compareDynamicWithTolerance(obj1, obj2, tolerance);
    }
} // namespace melon
